import unittest
import pickle
try:
    import cPickle
except ImportError:
    cPickle = None

from twisted.trial.unittest import TestCase
from twisted.internet import defer, reactor, task
from twisted.scheduling.tests.task import SimpleSchedule, IncrementingCallable

from meercat.job.interfaces import IJob
from meercat.job.scheduled import ScheduledJob



class TestException(Exception):
    pass

def failingCallable():
    raise TestException('fail')

class SimpleTests(TestCase):
    def setUp(self):
        super(SimpleTests, self).setUp()
        self.clock = task.Clock()
        self.schedule = SimpleSchedule(1)
        self.callable = IncrementingCallable()
        self.job = ScheduledJob(self.schedule, self.callable)
        self.job.getScheduledCall().clock = self.clock
    
    def test_starts_and_stops(self):
        """ Start and stop works """
        self.assertFalse(self.job.isRunning(),
                         u'Job should not be running before calling start')
        self.assertFalse(self.job.getScheduledCall().running,
                         u'Scheduled call should not be running before calling start')
        
        d = self.job.start()
        self.assert_(self.job.isRunning(),
                     u'Job should be running now')
        self.assert_(self.job.getScheduledCall().running,
                     u'Scheduled call should be running now')
        
        self.job.stop()
        self.assertFalse(self.job.isRunning(),
                         u'Job should not be running after calling stop')
        self.assertFalse(self.job.getScheduledCall().running,
                         u'Scheduled call should not be running after calling stop')
        return d
    
    def test_callable_called(self):
        """ Scheduled job calls callable """
        self.assertEqual(self.callable.count, 0, 
                         u'Callable should not be called before starting')
        d = self.job.start()
        self.assertEqual(self.callable.count, 0, 
                         u'Callable should not be called before time runs')
        self.clock.pump([0.1]*11)
        self.assertEqual(self.callable.count, 1,
                         u'Callable should have been called once')
        self.job.stop()
        self.assertEqual(self.callable.count, 1,
                         u'Callable should have been called once')
        return d
    
    def test_generate_deferred(self):
        """ Job._generateDeferred returns a deferred """
        self.assertIsInstance(self.job._generateDeferred(), defer.Deferred)
    
    def test_pickleability(self):
        """ ScheduledJob pickleable """
        self.assertIsInstance(self.job.__getstate__(), dict)
        pickled = pickle.dumps(self.job)
        self.assertIsInstance(pickled, basestring)
        self.assertIsInstance(pickle.loads(pickled), ScheduledJob)
        
        if cPickle:
            pickled = cPickle.dumps(self.job)
            self.assertIsInstance(pickled, basestring)
            self.assertIsInstance(cPickle.loads(pickled), ScheduledJob)
    

class FailingCallableTests(TestCase):
    def setUp(self):
        super(FailingCallableTests, self).setUp()
        self.clock = task.Clock()
        self.schedule = SimpleSchedule(1)
        
    
    def test_exception(self):
        """ Handle an exception raised by callable """
        def f():
            raise TestException()
        
        job = ScheduledJob(self.schedule, f)
        job.getScheduledCall().clock = self.clock
        
        def errback(failure):
            self.assert_(failure.check(TestException),
                         u'Expecting a TestException failure')
            self.assertEqual(failure, job.getError(),
                             u'Callback failure should match job.getError() failure')
            return
        
        d = job.start()
        d.addErrback(errback)
        
        self.assert_(job.isRunning(),u'Job should currently be running')
        self.assertEqual(job.getError(), None, u'Job.getError() should return None')
        self.clock.pump([0.1]*11)
        
        self.assertFalse(job.isRunning(),u'Job should have stopped with the error')
        self.assertNotEqual(job.getError(), None, u'Job.getError() should return an error now')
        return d
    
    def test_bad_arguments(self):
        """ Handle bad arguments sent to callable """
        def f(required_arg):
            pass
        
        job = ScheduledJob(self.schedule, f)
        job.getScheduledCall().clock = self.clock
        
        
        def errback(failure):
            self.assertEqual(failure, job.getError(),
                             u'Callback failure should match job.getError() failure')
            return
        
        d = job.start()
        d.addErrback(errback)
        
        self.assert_(job.isRunning(),u'Job should currently be running')
        self.assertEqual(job.getError(), None, u'Job.getError() should return None')
        self.clock.pump([0.1]*11)
        
        self.assertFalse(job.isRunning(),u'Job should have stopped with the error')
        self.assertNotEqual(job.getError(), None, u'Job.getError() should return an error now')
        return d
    

def test_suite():
    suite = unittest.TestSuite()
    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(SimpleTests))
    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(FailingCallableTests))
    return suite

if __name__ == '__main__':
    unittest.TextTestRunner(verbosity=2).run(test_suite())